Back

Introduction

In this tutorial, you will develop a Reverse Polish Notation (RPN) calculator. According to Wikipedia, PRN was developed by 1920 by Polish mathematician Jan Lukasiewicz, so the notation has some history. The purpose of this tutorial is to give you practice with BDD on an object with methods, rather than an object with one method, as in the first tutorial.

As with the first tutorial, this tutorial covers:
  • The three laws of TDD
  • Continuous refactoring
  • Basic continuous integration

Unlike the first tutorial, this tutorial will:
  • Spend more time discussing a few more of the S.O.L.I.D. principles,
  • Refactoring to patterns
  • Opening up classes using Ruby's Meta-Object Protocol

By the end of this tutorial, you'll be ready to start using story tests on a more complicated problem.

Note

This tutorial assumes you've either already worked through the first tutorial or are familiar with BDD and RSpec. If not, here are a few terms that come up early enough you'll need to know them:
  • Example - in RSpec parlance, this refers to a series of steps describing a use of the system. Before writing production code, you'll write an example. That example will not run most of the time, so you'll then create the production code necessary to get the example to pass.
  • Context - A context forms a common environment under-which Examples execute. A context may just be a name or it might include common setup and initialization as well as common clean-up. Contexts can be nested, which was demonstrated in the first tutorial.

Approach

Unlike the first tutorial, this tutorial will be a bit more explicit in its steps. Where reasonable, each Example will follow the same flow:
  • Title: Proceeded by a solid line, this will be at Heading Level 1
  • Overview: The overview immediately follows the title
  • Example: The Example be in a fixed-point font with different background.
  • Check-in: As with the other tutorials, getting you to practice frequent checkings is a secondary goal so there will be not-so-subtle reminders along the way. If you find your tools don't support such a working style, that's valuable information as well.
  • Refactor: There's almost always room for improvement, so the tutorial will review the work it just had you do and make suggestions as applicable. These refactorings are generally of the seconds-to-minutes variety. If an example requires a more significant restructuring, then it will be an entire sub-section in the Example.
  • Check-in: After refactoring, checking in again.
  • Summary: A synopsis of anything new or significant

There will also be occasional side-bars on an as-needed basis.

Sidebar.jpgSidebar: The Three Laws of TDD Revisited
Here are Robert Martin's three laws of TDD written from a BDD perspective:
  1. Write no production code without a failing Example
  2. Write only enough of an Example sufficient for it to fail (not compiling is failing)
  3. Write only enough production code to get the single failing Example to pass

These rules are not enough to effectively write code using either BDD or TDD. Here are a few more things to take into consideration:
  • Keep your code clean: as you notice unruly code, tame it. However, do this only when your Examples are all passing
  • Keep existing Examples passing
  • When you are looking at code, apply a critical eye and look for violations of sound design principles. (What are those principles? The tutorial will describe violations as they become ugly enough to warrant taking your time to discuss them.)
Sidebar.jpg

Before you get started on any Examples, however, we'll delve into the problem just a bit.

The Problem

An RPN calculator works by performing calculations on operands that the user has already entered. Here is an example scenario:
5 <enter>
3 
+
= 8
In this example, a user enters first 5 and then 3 and hits the + key to get the previous two operands 5 and 3, added together.

In general, using an RPN calculator obviates the need to use parenthesis. Consider the following:

That same expression could be calculated on an RPN calculator as follows:
5 <enter> 3 + 6 * 3 / 4 ^
=65536

An RPN calculator as a stack of previously entered values. The most recently entered numbers are the numbers that become available first (Last In First Out - LIFO). Consider this longer sequence:
3 <enter> 6 <enter> -8 <enter> 89 + sqrt - +
=0
Here's the evaluation of the above expression:
  • 3, 6, -8 get places on the operand stack
  • 8 is placed in "working storage" or in HP terminology, the x register
  • +, a binary operator, takes as its left hand operand the top item on the stack, -8 and as its right hand operand the x register, 89. The result, 81, is placed in the x register
  • Next, sqrt, a unary operator, takes the value on the x register and calculated 9, which is then put into the x register
  • The first -, another binary operator, takes as its left-hand operator the top value on the stack, which is currently 6. It takes as its right-hand operator the x register, which is 9. The result, -3, is placed in the x register
  • The first -, another binary operator, takes as its left-hand operator the top value on the stack, which is currently 6. It takes as its right-hand operator the x register, which is 9. The result, -3, is placed in the x register.
  • The final operator, +, another binary operator, takes as its first operand the top item on the stack, which is 3. For its right-hand operand, it takes the x register, -3. Adding, the result is 0, which becomes the value in the x register.
  • The stack is empty.

The x register

The x register behaves like an accumulator. As you type numbers, those numbers get added to what is already there. When you type a non-number key, it applies itself. For example, the <enter> key really behaves like a unary operator. When you press <enter>, it takes the value in the x register and places it on the stack.
  • If the next key is part of a number, the x register is reset and the key pressed becomes the contents of the x register.
  • If the next key is not part of a number, then it applies as is.

After any non-number key, the x register is "locked" and any attempt to change it results in it being reset.

Getting Started

There are several major parts to this problem:
  • Handling input
  • Handling the stack and the x register properly
  • Processing the basic operators (+, -, /, *, ^)
  • Handling more complex functions (e.g., prime factors, sum over the stack)

In addition, we'll add several things to make this a bit more interesting:
  • CRUD'ing (Create, Read, Update, Delete) variables
  • CRUD'ing user-defined functions

Behavior Driven Development advocates starting outside in; start with the user experience and then work your way into the system. In this tutorial, you will not strictly be creating a user interface; you will, however, create the stuff necessary to build a user interface. The tutorial will start at the individual entry of keys and work up to basic math.

Your Work Area

  • Create a directory to store all of your work. Unlike the first tutorial, you'll be working in multiple files this time.

The 0th Example

As with all problems, you need to pick a starting place. As with the first tutorial, you will create a basic example describing object creation. Next, you'll continue with a series of examples centered on basic input. Thinking further ahead at this point might constitute analysis paralysis.

Example
Here is a first Example for this problem:
describe "Basic Creation" do
  it "should be non-null after creation" do
    calculator = RpnCalculator.new
  end
end
  • Create this Example in your directory, call the file "rpn_calculator_spec.rb"

  • Execute the Example, it fails:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    Basic Creation
    - should be non-null after creation (ERROR - 1)
     
    1)
    NameError in 'Basic Creation should be non-null after creation'
    uninitialized constant RpnCalculator
    ./rpn_calculator_spec.rb:3:
     
    Finished in 0.013091 seconds
     
    1 example, 1 failure
The -c option adds color to the output. You won't see the color in this tutorial. Also, if you are running this in a DOS terminal window, then you'll want to leave this option off.

The message describes an unknown constant, RpnCalcualtor. You'll need to create that class.

  • Create the following Ruby class in a file called rpn_calculator.rb:
    class RpnCalculator
    end

  • Update rpn_calculator_spec.rb to require the file you just created:
    require 'rpn_calculator'
     
    describe "Basic Creation" do
      ...
    end

  • Run your Example again, you should be all green:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    Basic Creation
    - should be non-null after creation
     
    Finished in 0.0123 seconds
     
    1 example, 0 failures

OK, you have an Example but it does not contain any verification. So one more change and one more execution.

  • Update the Example to validate calculator:
        calculator.should_not be nil

  • Run your Example again, you should be all green:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    Basic Creation
    - should be non-null after creation
     
    Finished in 0.0123 seconds
     
    1 example, 0 failures

Sidebar.jpgSidebar: Examples passing without validation
Generally, we want Examples to fail for an expected reason before they pass. This case is an exception to that rule, but you'll see that coming up.
Sidebar.jpg

Sidebar.jpgSidebar: All Green
All green refers to the state of the executing Example; it means all Examples are executing and passing. This terminology derives from unit testing in Smalltalk and then popularized by JUnit. In Smalltalk, their process was:
  • Red: Create a failing test
  • Green: Get the failing test to pass
  • Blue: Refactor

This is essentially what this tutorial advocates. JUnit turned this into red-bar, green-bar. When the GUI of JUnit executes, it has a progress bar that indicates either:
  • Red: one more more tests have failed
  • Green: all tests executed so far have passed
Sidebar.jpg

Check-In
You are all green, so it is time to check in your work. As with the previous tutorial, I'll be using git:
Macintosh-7% ls
rpn_calculator.rb  rpn_calculator_spec.rb  wikiwriteup.txt
Macintosh-7% git init
Initialized empty Git repository in /Users/schuchert/src/ruby/bdd_tutorial_2/.git/
Macintosh-7% git add *   
Macintosh-7% git commit
Created initial commit 05273da: First spec passing.
 3 files changed, 354 insertions(+), 0 deletions(-)
 create mode 100644 rpn_calculator.rb
 create mode 100644 rpn_calculator_spec.rb
 create mode 100644 wikiwriteup.txt

Note that in this example I have a file you will not have, wikiwriteup.txt. That's the raw material used for format the wikipage you are now reading.

Refactor
A quick review of the existing Example and production code does not suggest any refactoring yet.

Check-In
Since there was no need to refactor, there's no need to check in again.

Summary
You created two files:
rpn_calculator_spec.rb
require 'rpn_calculator'
 
describe "Basic Creation" do
  it "should be non-null after creation" do
    calculator = RpnCalculator.new
    calculator.should_not be nil
  end
end
rpn_calculator
class RpnCalculator
end

This may seem like a trivial start, and that is by design. What have you verified so far:
  • You have a working environment
  • The RSpec gem is installed
  • Ruby is intalled
  • You have the basic files in place
  • You've checked your work into a repository

Not every Example will be so small and quick to create, but you should attempt to break your work down into bite-sized chunks like this. Why?
  • If you get distracted, you don't lose very much ground.
  • If you mess up and need a do-over, you don't lose too much work.
  • If someone needs your attention, you're not too far from having a great place to stop what you're doing.
  • You can feel a sense of accomplishment throughout the day.
  • You can watch as your system organically grows from something trivial, to something complex, typically quickly.

Sidebar.jpgSidebar: A Concrete Example of a Hardware Simulator
In mid 2008, I worked with a great group of people on the East Coast. My task was to use their current problem as source material and to take them through TDD (not BDD) on their problem.

They already had a start on the problem and had pretty good source material on what the system needed to do. The system essentially simulated expensive hardware. This system would make testing the software to drive the expensive hardware. The problem involved capturing an over-the-wire protocol, which they were already doing in C++. The problem, then, was to take that protocol and respond appropriately and even initiate messages.

During the week I was there, we:
  • Created an initially simple solution that evolved into something that nearly simulated a complete, albeit simple, "job".
  • We had fully tested the execution of the protocol, including starting the system up in simulated mode (staring a Java virtual Machine [JVM] directly) versus real mode, starting a JVM from a C++ application.

On Tuesday, the week after I left, they had managed to fix a few problems on protocol capture and had successfully processed a job. On Wednesday, they were successfully processing a number of simple jobs.

A few weeks later they were processing complex jobs and then a few weeks later they were processing up to 15 simultaneous complex jobs. Their simulator was fast enough that they had to slow it down.

In a very real sense, starting with a granule and letting the system grow through accretion is a viable way to build complex systems.
Sidebar.jpg

Example: Accepting User Input

With the first Example running it is time to move on to basic user input. We have to make a decision already, where is the system boundary. Here are a few possibilities:
  1. The system receives a series of events for buttons pressed, e.g., pressing "9" on the UI generates a 9-button pressed event, pressing "sqrt" on the UI generates a sqrt-button pressed event.
  2. The system receives "digit" messages for digits or "function" messages for functions.
  3. The system receives messages such as a complete number, enter, + and so on.

There are other alternatives, and to some extent, the selection is arbitrary. There is no decision on the UI technology there's no way to know the eventing mechanism, if applicable. Even so, you can make progress based on some logical representation and then adapt between the real UI technology and the system you've developed.

A standard model for handling user input is the Model:View:Control pattern. In this case, the View is the as-yet-to-be-determined presentation technology. The Model is the calculator itself. The control part of the equation maps between the view and the model (the UI and the calculator). So you will create the model, drive the system using RSpec as the de-facto control, and when you've selected the UI technology, you'll create a new control to map from the UI to your well-tested model.

As a side benefit, most of the logic will be in easy to test code.

Since the particulars of the UI are irrelevant, this tutorial will move forward using option 2 listed above for handling numbers. When it is time to handle things like +, /, etc., it'll be time to make another decision.

Example
Here is the beginning of an example:
describe "Handling User Input of Numbers" do
  it "should store a series of digits in the x register" do
    calculator = RpnCalculator.new
    calculator.digit_pressed '4'
  end
end

There's more to be written, but as soon as the message "digit_pressed" is sent to a calculator, the Example will fail.

  • Create this Example, adding it to rpn_calculator_spec.rb.

  • Execute the Example, you should see an error similar to the following:
    1)
    NoMethodError in 'Handling User Input of Numbers should store a series of digits in the x register'
    undefined method `digit_pressed' for #<RpnCalculator:0x58kkc80c>
    ./rpn_calculator_spec.rb:12:
     
    Finished in 0.015707 seconds
     
    2 examples, 1 failure

Assuming you worked through the first BDD tutorial, this is familiar ground. You need to add a method to get this to compiled (you've just finished applying law 2 of the 3 laws of BDD).

  • Update RpnCalculator:
    class RpnCalculator
      def digit_pressed(digit)
      end
    end

  • Verify that your Example now executes successfully.

  • Complete the Example:
    describe "Handling User Input of Numbers" do
      it "should store a series of digits in the x register" do
        calculator = RpnCalculator.new
        calculator.digit_pressed '4'
        calculator.digit_pressed '2'
        calculator.x_register.should == 42
      end
    end

  • Execute your examples, one will fail:
    1)
    NoMethodError in 'Handling User Input of Numbers should store a series of digits in the x register'
    undefined method `x_register' for #<RpnCalculator:0x58c5dc>
    ./rpn_calculator_spec.rb:14:
     
    Finished in 0.015999 seconds
     
    2 examples, 1 failure

As with the previous failure, this failure is a result of a missing method.

  • First, create the method in RpnCalculator to get your code to compiling:
      def x_register
      end

  • Run your Example, make sure it is failing:
    1)
    'Handling User Input of Numbers should store a series of digits in the x register' FAILED
    expected: 42,
         got: nil (using ==)
    ./rpn_calculator_spec.rb:14:
     
    Finished in 0.016444 seconds
     
    2 examples, 1 failure

  • Now, do the simplest thing that could work, AKA fast-green bar, AKA, Uncle Bob's TDD rules, #3:
      def x_register
        42
      end

  • Finally, run your Examples to make sure they are passing:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    Basic Creation
    - should be non-null after creation
     
    Handling User Input of Numbers
    - should store a series of digits in the x register
     
    Finished in 0.014956 seconds
     
    2 examples, 0 failures

Sidebar.jpgSidebar: Why two steps instead of just one?
Why not just create a method and in the same step get your Example passing? In this trivial example you could and it would not be too risky. However, in general you should make sure your Examples fail for the right reason before getting them to pass. Why? It serves as a check that the Example you've written is doing what you think it is doing.

With proper IDE support, the intermediate failing step to the working step is seconds. Even with the environment I'm using, two terminal windows, the work is 10 - 15 seconds. So while this is "extra" work, it has a purpose.

Eventually you will see these tutorials skipping steps. But for now, the tutorial is keeping the rigor high.
Sidebar.jpg

Sidebar.jpgSidebar: What a silly implementation!
What about the hard-coded return value? Why not just write the code now, it's clear what needs to be done. This is an issue of balancing what you know and the risk that what you know is incorrect.

In this case, you could probably guess where this is going. However, as I write the tutorial, this is literally the first time I've worked on this part of this problem, so while I think I know what's going to happen, I'm really not sure. And even if I know the next step, I am not too sure about 2, 3 or more steps ahead.

A general answer to this complaint is that rather than writing what you know the solution should be, write an Example that will force/allow you to write the code you know should be there.

The primary danger of jumping ahead is writing too much production code. That code may not be well covered by your Examples, so you'll essentially have unvalidated code. In addition, you could write more than is necessary. If you do that, the next person to read the code might wonder why there's more code than the necessary. You might be that next person, by the way.
Sidebar.jpg

Check-In
  • Check in your work:
    Macintosh-7% git commit -a
    Created commit 727745f: Added basic support for handling digits.
     3 files changed, 215 insertions(+), 2 deletions(-)

Refactor
Review of the RpnCalculator class does not suggest any refactoring.

Review of the Examples, however, does. There is some minor duplication. Both Examples create a calculator. This is a quick fix since you can create a containing Context with common setup. As with any refactoring, however, you'll take small steps and verify as you go along.

  • Create a containing context:
    Above the first describe
    describe "RPN Calculator" do

    After the last end
    end

    Update the spacing - that's one command in VI, the best-ever editor
    describe "RPN Calculator"
      describe "Basic Creation" do
        ...
      end
     
      describe "Handling User Input of Numbers" do
        ...
      end
    end

  • Verify that your Examples still execute:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    RPN Calculator
     
    RPN Calculator Basic Creation
    - should be non-null after creation
     
    RPN Calculator Handling User Input of Numbers
    - should store a series of digits in the x register
     
    Finished in 0.016923 seconds
     
    2 examples, 0 failures

  • Add a common setup to your outer-context:
      before(:each) do
        @calculator = RpnCalculator.new
      end

  • Verify your Examples still run.

  • Update the first Example to use the initialized calculator:
      describe "Basic Creation" do
        it "should be non-null after creation" do
          @calculator.should_not be nil
        end
      end

  • Verify your Examples still run.

  • Update the second Example as well:
      describe "Handling User Input of Numbers" do
        it "should store a series of digits in the x register" do
          @calculator.digit_pressed '4'
          @calculator.digit_pressed '2'
          @calculator.x_register.should == 42
        end
      end

  • Verify your Examples still run.

You might be tempted to do more at this point. For example, you know you'll be adding a lot of values to the x_register. However, let that generalization happen naturally. Practicing both BDD and TDD encourages bottom-up, example-based generalization.

Check-In
  • Check in your work:
    Macintosh-7% git commit -a
    Created commit b203185: Removed duplication of calculator initialization.
     2 files changed, 128 insertions(+), 18 deletions(-)
     rewrite rpn_calculator_spec.rb (90%)

Summary
The first example added basic object creation. This Example defined some of the API for entering digits. The production code doesn't actually process this yet, however, you already have executable documentation of how the API is supposed to work.

You also performed some basic refactoring on your Examples. While mentioned in the first tutorial, here are a few observations:
  • You want to get to the point where your muscle memory is imbued with this kind of basic cleanup. The only way to get there is practice.
  • We removed a violation of the DRY principle. I believe Ruby Dave Thomas is credited with saying all design is an exercise in removing duplication. Regardless of who said it, I think it's a sound idea.

Example: Taking a decimal point as well

The production code is hard-coded. Somehow you want to push your solution to remove that hard-coded value. One way to do that is to and another Example (don't do this):
  •   describe "Handling an even longer User Input of Numbers" do
        it "should store a series of digits in the x register" do
          @calculator.digit_pressed '1'
          @calculator.digit_pressed '2'
          @calculator.digit_pressed '4'
          @calculator.digit_pressed '3' 
          @calculator.digit_pressed '2' 
          @calculator.x_register.should == 12432
        end

On the one hand, this will force some more work in the production code. On the other hand, this Example is a proper superset of the first Example. That is, this Example completely covers the first Example, so, pragmatically, you should remove the first Example. More Examples does not necessarily mean better Examples. Each Example should push the solution a little further and avoid duplicating the work of another Example. Why?
  • More examples --> longer to run --> run less often --> less effective
  • When something breaks, you are likely to break more than one Example --> more maintenance --> Examples become a hassle to maintain --> Examples are commented out or left not passing --> nearly defeats the purpose of BDD.

Sidebar.jpgSidebar: Failing Tests: Not Idle Chit-Chat
This is not idle speculation, I've been on projects where this dynamic occurred. One example involved a situation where "certain tests" (this is TDD, not BDD) just failed and people tended to not worry about them. A developer added a new key-value pair to a hash table. This caused a test to fail. He figured that the test was failing for some other reason, so he ignored it.

In fact here's what was happening:
  • The hashtable was in an object being written as a blob
  • The new key-value pair pushed the object just over the limit of the blob size (4076 bytes -- 5000 bytes, limit was 4096)
  • So his change was breaking the test, but not always because sometimes the data was just small enough to fit.

If tests fail for the wrong reasons, it makes it easy for someone to assume that what they just did could not possibly break the test. Often that just means an assumption is about to be exposed for what it is.

The DRY principle also covers test coverage.

So what can you do? How about accepting a decimal point in the number?
Sidebar.jpg

Example
Here is another example that fits extends the functionality just a little but will also force the implementation of the production code:
    it "should handle a string of digits with an embedded ." do
      @calculator.digit_pressed '1' 
      @calculator.digit_pressed '3' 
      @calculator.digit_pressed '.'
      @calculator.digit_pressed '3'
      @calculator.digit_pressed '1'
      @calculator.x_register.should == 13.31
    end

  • Create this Example.

  • Run your Examples, you should have one failure:
    ... <snip> ...
    - should handle a string of digits with an embedded . (FAILED - 1)
     
    1)
    'RPN Calculator Handling User Input of Numbers should handle a string of digits with an embedded .' FAILED
    expected: 13.31,
         got: 42 (using ==)
    ./rpn_calculator_spec.rb:27:
     
    Finished in 0.018738 seconds
     
    3 examples, 1 failure

Now it is time to do something more than hard-code the response.

  • Add an initialization method to your RpnCalculator class:
      def initialize
        @input = ''
      end

  • Update the digit_pressed method to record the digit:
      def digit_pressed(digit)
        @input << digit.to_s
      end

  • Update the x_register method to use the recorded input:
      def x_register
        @input.to_f
      end

  • Run your Examples, you should be all green:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    RPN Calculator
     
    RPN Calculator Basic Creation
    - should be non-null after creation
     
    RPN Calculator Handling User Input of Numbers
    - should store a series of digits in the x register
    - should handle a string of digits with an embedded .
     
    Finished in 0.018312 seconds
     
    3 examples, 0 failures
Check-In
  • It is once again time to check in your code. Everything is green:
    Macintosh-7% git commit -a
    Created commit e10de87: Added support for numbers with decimals.
     3 files changed, 135 insertions(+), 2 deletions(-)

And as a reminder, the reason there are so many insertions in this example is because my checkins include updates to the source file used to generate the page you are reading.

Refactor
Reviewing the RpnCalclator class, there's little to do right now. What about your Examples? There appears to be duplication in that there are several lines that call the digit_pressed method. Is this duplication? It does represent the underlying objects' API. Even so, how can you improve this?

Before making any improvements, you need a reason to make the improvement. It is to make the Example more readable, easier to write, easier to maintain, or some other characteristic?

How about allowing your Example to provide a number, which is then "typed" for you?

  • Add a method to the containing context:
    describe "RPN Calculator" do
      ...
      def enter_number(number)
        number.to_s.each_char{ |d| @calculator.digit_pressed d }
      end
      ...
    end

  • Run your Examples, they should still pass.

  • Update the "Handling User Input of Numbers" example:
        it "should store a series of digits in the x register" do
          enter_number 42
          @calculator.x_register.should == 42
        end

  • Run your Examples, they should still pass.

  • Update the "should handle a string of digits with an embedded ." Example:
        it "should handle a string of digits with an embedded ." do
          enter_number 13.31
          @calculator.x_register.should == 13.31
        end

  • Run your Examples, they should still pass.

  • Finally, the second Example does not start with should, which is a pretty well-followed practice. So fix it:
      describe "Handling integers" do
        it "should store a series of digits in the x register" do
          enter_number 42
          @calculator.x_register.should == 42
        end


  • Run your Examples, they should still pass.
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    RPN Calculator
     
    RPN Calculator Basic Creation
    - should be non-null after creation
     
    RPN Calculator Handling integers
    - should store a series of digits in the x register
    - should handle a string of digits with an embedded .
     
    Finished in 0.017654 seconds
     
    3 examples, 0 failures

Check-In
  • Check in your changes:
    Macintosh-7% git commit -a
    Created commit a465d16: Removed duplication in writing examples taking numbes.
     2 files changed, 56 insertions(+), 24 deletions(-)

Summary
Congratulations, you have basic support for processing numeric input. You managed to use an Example that pushed the design just a bit and ended up with code in each of the methods.

There is one strange thing, your code uses "digit_pressed" for the digits 0 through 9 and '.'. That's something worth considering.

Here is a snapshot of what your code should resemble at this point:
rpn_calculator_spec.rb
require 'rpn_calculator'
 
describe "RPN Calculator" do
  before(:each) do
    @calculator = RpnCalculator.new
  end
 
  def enter_number(number)
    number.to_s.each_char{ |d| @calculator.digit_pressed d }
  end
 
  describe "Basic Creation" do
    it "should be non-null after creation" do
      @calculator.should_not be nil
    end
  end
 
  describe "Handling integers" do
    it "should store a series of digits in the x register" do
      enter_number 42
      @calculator.x_register.should == 42
    end
 
    it "should handle a string of digits with an embedded ." do
      enter_number 13.31
      @calculator.x_register.should == 13.31
    end
  end
end

rpn_calculator.rb
class RpnCalculator
  def initialize
    @input = ''
  end
 
  def digit_pressed(digit)
    @input << digit.to_s
  end 
 
  def x_register
    @input.to_f
  end
end 

Example: What Happens with Enter?

What happens when a user presses the <enter> key on the calculator:
  • The current value in the x_register is placed on the stack of previous operands.
  • Subsequent, the next numeric digit causes the x_register to reset, but pressing <enter> again causes that value to placed on the stack again.

In fact, as you will see, pressing a non-numeric key generally "locks-down" the x_register and makes the next digit cause it to be reset. You can describe this we several Examples.

Example
Here is a Context with three as yet to be defined Examples:
  describe "Handling the <enter> key" do
    it "should copy x_register to top of stack" 
 
    it "should cause next digit to reset the x_register" 
 
    it "should allow the x_register to be <entered> again" 
  end

  • Add this context as a sub-context of the RPN Calculator context.

  • Execute your Examples, you should notice that these three are listed as pending:
    RPN Calculator Handling the <enter> key
    - should copy x_register to top of stack (PENDING: Not Yet Implemented)
    - should cause next digit to reset the x_register (PENDING: Not Yet Implemented)
    - should allow the x_register to be <entered> again (PENDING: Not Yet Implemented)
     
    Pending:
    RPN Calculator Handling the <enter> key should copy x_register to top of stack (Not Yet Implemented)
    RPN Calculator Handling the <enter> key should cause next digit to reset the x_register (Not Yet Implemented)
    RPN Calculator Handling the <enter> key should allow the x_register to be <entered> again (Not Yet Implemented)
     
    Finished in 0.022477 seconds
     
    6 examples, 0 failures, 3 pending

Before you can write any of these, you have to make a decision about how to indicate that the <enter> key was pressed. Here are a few options:
  1. Add a method, enter_pressed.
  2. Add a method, execute_function(:enter)

Is there a clear winner? There are trade-offs:
  • The first option suggests a "wide" API. That is, the number of methods on the PN Calculator grows as its functionality grows. This might sound "normal" but it could be a violation of the Open/Closed Principle.
  • The second option requires a symbol for each function.
  • The second option requires the RPN calculator to map from a symbol, :enter, to a behavior.

Sidebar.jpgSidebar: Open/Closed Principle
The Open/Closed Principle suggests that a class, once released into the wild, should not change again except for fixing errors. In Eiffel, this meant leaving a class alone and extending from it to add new behavior. In general, this can also mean depending on an abstraction (interface or abstract base class) and then adding subclasses to complete the behavior.

Why is this relevant? Gerald M. Weinberg describes a relationship between the size of a bug fix and the likelihood that the bug fix introduces new bugs. In his experience, the smaller the fix, the more likely a new bug will be introduced. In his research, 66% of all one-line bug fixes introduce new bugs.

So leaving working code alone is at the heart of the Open/Closed Principle.

Adding new methods to an existing class is in fact changing that class. Adding new methods to a class is less likely to cause problems, but it is not impossible break a class when you do add methods to it. This suggests adding methods to a class every time we add a new feature is probably a bad idea.

Another issue is, in general, the more methods on a class, the more likely it is that it will be hard to understand. Finally, having many methods on a class makes it more likely that the class will violate the Single Responsibility Principle.
Sidebar.jpg

This tutorial will take the second approach.

  • Add to the first Example (make sure to add the 'do' after the first line):
        it "should copy x_register to top of stack" do
          enter_number 654
          @calculator.execute_function(:enter)
        end

Why stop here? This Example is now failing because it invokes a method that does not yet exist.

  • Run your Examples to verify this:
    1)
    NoMethodError in 'RPN Calculator Handling the <enter> key should copy x_register to top of stack'
    undefined method `execute_function' for #<RpnCalculator:0x587ac8 @input="654">
    ./rpn_calculator_spec.rb:33:
     
    Finished in 0.023074 seconds
     
    6 examples, 1 failure, 2 pending

  • Get the example back to Green by adding a method in RpnCalculator:
      def execute_function(function_symbol)
      end

  • Continue with the Example:
        it "should copy x_register to top of stack" do
          enter_number 654
          @calculator.execute_function(:enter)
          @calculator.top.should == 654
        end

This both completes the Example and causes it not to work because the top method does not exist. You can verify this by running your Examples.

  • Add a top method:
      def top
        654
      end

Did you notice I just had you "cheat". You should have:
  • Created a method that would cause the Example to fail.
  • Run your Examples.
  • Updated the method to return 654
  • Run your Examples.
Did you notice that? If not, see how easy it is to slide backwards?

Sidebar.jpgSidebar: Just how much can you follow the three laws
If I have a proper IDE and decent refactoring tools, then I really do prefer following the three laws strictly. Unfortunately, I have not as of 2008/10 found an IDE that has two particular refactorings/corrections for Ruby:
  • Create class missing at the cursor location (preferably in its own file)
  • Create method missing at the cursor location

I have these in C++, C#, Java, VB.Net and others. Since I don't have these, I'd consider relaxing the second rule of TDD to allow me to write an entire Example and then get add the missing Class/methods. I am not taking this approach in the tutorial because it doesn't add that much time and I'd rather you see how it is supposed to be practiced.

Is this realistic? Yes. Many developers better than I have practiced the three laws. In the simulator mentioned above, during the week I was there, we strictly followed TDD. I was not always at the keyboard but the work was being projected on an 1080i overhead projector so that kept the developers "honest."
Sidebar.jpg

Check-In
You have all passing Examples, but two are not implemented. Should you check in your work?
  • Sure:
    Macintosh-7% !g
    git commit -a
    Created commit f77e000: Added support for the "enter" function.
     3 files changed, 148 insertions(+), 7 deletions(-)

Refactor
A quick review of RpnCalculator doesn't suggest a need for refactoring. For now, the same can be said of the Examples.

Check-In
No need, there was no need to refactor.

Summary
Do you think pressing <enter> on a calculator is a function? Before I started this project my reaction would have been, "That's silly." However, when I got to this Example, I considered the writeup on the RPN Calculator and realized that the <enter> key is a function just as much as sqrt, pow, etc.

Have we made progress? Yes, we have extended the API of the calculator. What about checking in code with pending Examples, is that a good idea? I think it is OK, but it could cause problems. I prefer clean Example execution, so having several pending Examples is a bother. On the other hand, if I'm pairing with someone and one of us thinks of something worthy of validation, but it is not where we are working, we could write it down, email it, yell it to another team member, or we can put it in the code in an executable fashion.

Example: Checking that the x_register is reset-able

The next Example involves what happens after the <enter> key. You could have included this check in the previous Example, however keeping the tests small, focused and checking fewer things makes pinpointing problems easier.

Example
Here's an example that seems to express the idea:
    it "should cause next digit to reset the x_register" do
      enter_number 654
      @calculator.execute_function(:enter)
      enter_number 1
      @calculator.x_register.should == 1
    end

  • Create this Example and then run you Examples:
    1)
    'RPN Calculator Handling the <enter> key should cause next digit to reset the x_register' FAILED
    expected: 1,
         got: 6541.0 (using ==)
    ./rpn_calculator_spec.rb:41:
     
    Finished in 0.023285 seconds
     
    6 examples, 1 failure, 1 pending

Notice that the 1 entered was appended to the x_register. So looks like this test pushes our current production code. You might also have observed that the support method named "enter_number" is really a bit off. Since <enter> has a meaning now, this method's name should be changed. That, however, is a refactoring so you should wait until you have all Examples passing (it OK to leave the pending Example pending).

  • Update the calculator to work with the new Example:
      def digit_pressed(digit)
        @input = '' if @x_register_should_reset
        x_register_should_reset = false
        @input << digit.to_s
      end
     
      def execute_function(function_symbol)
        @x_register_should_reset = true
      end

  • Run your Examples, they should pass:
    RPN Calculator Handling the <enter> key
    - should copy x_register to top of stack
    - should cause next digit to reset the x_register
    - should allow the x_register to be <entered> again (PENDING: Not Yet Implemented)
     
    Pending:
    RPN Calculator Handling the <enter> key should allow the x_register to be <entered> again (Not Yet Implemented)
     
    Finished in 0.022283 seconds
     
    6 examples, 0 failures, 1 pending

Check-In
  • Now is a good time to check in, even though you have a pending Example:
    Macintosh-7% !g
    git commit -a
    Created commit d8902e3: Added resetting of x_register after :enter
     3 files changed, 65 insertions(+), 3 deletions(-)

Refactor
Here's a list of things that you might consider for refactoring:
  • There's a violation of DRY between digit_pressed and initialize
  • There's some duplication between the last two Examples
  • The method name enter_number either needs to be changed, or it should call the <enter> method.

enter_number
You can either change its name or make it do what it says. Flip a coin, you probably do not have enough information to make that decision just yet. However, in the spirit of keeping things clean, I did make a decision. I kept the method name and changed its implementation:
  •   def enter_number(number)
        number.to_s.each_char{ |d| @calculator.digit_pressed d }
        @calculator.execute_function(:enter)
      end

  • Run your Examples, nothing is broken.

Violation of DRY between digit_pressed and initialize
This is one of those cases where duplication might be OK because right now initialize is simple but it is likely to get more complex later. I was originally going to change the implementation to this (don't do this):
  def digit_pressed(digit)
    initialize if @x_register_should_reset
    x_register_should_reset = false
    @input << digit.to_s
  end

However, before I made that change, it occurred to me that coupling to the initialize method will probably cause problems later. Even though I'd catch that when Executing my Examples, I instead decided to introduce a simple method that documents the intent.

  • Add a new method:
      def reset_input
        @input = ''
      end

  • Verify that nothing broke.

  • Update initialize and digit_pressed to use this new method.

  • Verify that nothing broke.

Duplication in last two Examples
When you changed the implementation of enter_number to actually perform an <enter>, you could have updated the last two Examples as follows:
    it "should copy x_register to top of stack" do
      enter_number 654
      @calculator.top.should == 654
    end
 
    it "should cause next digit to reset the x_register" do
      enter_number 654
      @calculator.digit_pressed 1
      @calculator.x_register.should == 1
    end

When these Examples reflect the update to the enter_number method, the duplication does not see to be bad.

Check-In
  • Check in your work.
    Macintosh-7% !g
    git commit -a
    Created commit a8fec92: Refactored input resetting, enter_number now 
     3 files changed, 75 insertions(+), 8 deletions(-)

Summary
The behavior of the calculator has grown. The it also is beginning to handle the <enter> functionality. You removed duplication in the production code. It was small, and trivial and probably seemed like nothing. However, here's a principle from Jerry Weinberg:


If you are not maintaining your code and constantly cleaning it up, then it is decaying. Even if your code is not changing, it is decaying because the market is changing, the users' understanding of your system is changing, something is changing.

Keeping your code clean is not an event, it's not a processes, it is an attitude. You have it or you do not have it.

Are you going to be perfect?
  • Perfection is not a destination, it is a journey.

No, you are going to miss stuff. That's why when you do find something, now is the time to handle it. If you cannot deal with it now, write it down on a piece of paper. Throw that piece of paper away in 2 days. If you haven't found the time to fix it by then, then by keeping the paper, you're just giving yourself undue stress.

Example: Should allow x_register to be entered again

This one has been pending. While I was just about to get to that, a few ideas went through my head:
  • There's going to be a stack of numbers somewhere.
  • Once the calculator starts resetting, the code never stops, so the Examples have left work on the plate.

Those two items are officially on the punch-list. For now, let's get back to all green. We've been away from all green for too long.

Example
  • Create the following example:
        it "should allow the x_register to be <entered> again" do
          enter_number 654
          @calculator.execute_function(:enter)
          @calculator.top.should == 654
          @calculator.x_register.should == 654
        end

  • Run your Examples:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    RPN Calculator
     
    RPN Calculator Basic Creation
    - should be non-null after creation
     
    RPN Calculator Handling integers
    - should store a series of digits in the x register
    - should handle a string of digits with an embedded .
     
    RPN Calculator Handling the <enter> key
    - should copy x_register to top of stack
    - should cause next digit to reset the x_register
    - should allow the x_register to be <entered> again
     
    Finished in 0.022528 seconds
     
    6 examples, 0 failures

Well everything is all green. So this Example might not be adding value. Rather than immediately delete it, keep a note to review it after completing the first two items on the punch-list.

Check-In
Should you check in? You might not be sure about the Example, but you've got that to review after just a few more Examples. Even so, go ahead and commit. If you remove the Example later, your RCS tool can handle the workload.

  • Commit:
    Macintosh-7% !g
    git commit -a
    Created commit 437ee9f: Added candidate Example. Might remove it shortly,.
     2 files changed, 51 insertions(+), 8 deletions(-)

Refactor
There are some duplicated validation steps in the Examples. Before refactoring, however, keep things as is until you've done just a little more work.

Check-In
There's nothing to check-in right now.

Summary
You've added an Example. You are not sure if it is a good one or not. You've got two items on your punch list. Review this Example after you have addressed those other two items.

Example: Resetting after the first digit

Once an example sends the execute_function method, the RpnCalculator will continue to reset. You need to fix this.

Example
  • Create the following example.
        it "should only reset after the first digit and not subsequent digits" do
          enter_number 654
          enter_number 27
          @calculator.x_register.should == 27
        end

  • Run your Examples, notice the failure:
    - should only reset after the first digit and not subsequent digits (FAILED - 1)
     
    1)
    'RPN Calculator Handling the <enter> key should only reset after the first digit and not subsequent digits' FAILED
    expected: 27,
         got: 7.0 (using ==)
    ./rpn_calculator_spec.rb:53:
     
    Finished in 0.023869 seconds
     
    7 examples, 1 failure

The problem is, the "reset state" is set in execute_function but never reset.

There are two ways to fix this:
Update the digit_pressed method
  def digit_pressed(digit)
    if @x_register_should_reset
      reset_input 
      @x_register_should_reset = false
    end
    @input << digit.to_s
  end

Or Update the reset_input method
  def reset_input
    @input = ''
    @x_register_should_reset = false
  end

Either will work. In fact, doing both works. Question, to which method does the responsibility seem to bind? If you tought reset_input, you're right.

  • Fix this by updating the reset_input method.

  • Make sure your Examples pass:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb
     
    RPN Calculator
     
    RPN Calculator Basic Creation
    - should be non-null after creation
     
    RPN Calculator Handling integers
    - should store a series of digits in the x register
    - should handle a string of digits with an embedded .
     
    RPN Calculator Handling the <enter> key
    - should copy x_register to top of stack
    - should cause next digit to reset the x_register
    - should allow the x_register to be <entered> again
    - should only reset after the first digit and not subsequent digits
     
    Finished in 0.025639 seconds
     
    7 examples, 0 failures

Sidebar.jpgSidebar: I did too much
When I wrote "the solution" to the "should cause next digit to reset the x_register" Example, I put this logic in my code. I then realized that it was doing nothing to support any Examples so I removed it.

I then decided to create a test, the one you just worked on, to justify the need to reset the variable. If I had been working with a pairing partner, I'd hope my co-pilot would have called me on violating the third rule of TDD/BDD.
Sidebar.jpg

Check-In
  • Check in your work, you've made some good progress:
    Macintosh-7% !g
    git commit -a
    Created commit e1a018b: Added Example to verify that resetting was turned off.
     3 files changed, 139 insertions(+), 5 deletions(-)

Refactor
Reviewing the RpnCalculator there does not seem to be a need to do any refactoring. I do observe that the top method is still hard-coded, so an Example to drive that would be good.

As for the Examples, there is one thing you can do to clean up a little duplication. Every Example enters the number 654, so add a before method:
    before(:each) do
      enter_number 654
    end

  • Add the before method and run your Examples. Make sure all Examples still pass.

  • Now, remove the first enter_number 654 from each of the subsequent Examples.

  • Verify that all of your Examples still pass.

Check-In
With the one refactoring, now is a good time to check in your changes

Summary
You've pushed the solution a bit further and cleaned up some duplication. Specifically, resetting the value in the x_register seems to work and you've put some common setup work into a before for your Context.

You still have the question of whether there should or should not be a stack. And what about that pesky Example? You're keeping a punch-list, right?

Example: Enter actually puts values on the stack

Up to this point, your examples have addressed the effect of the <enter> key on the x_register, but what about stored values? Consider the following:
7 <enter>
14 <enter>
99
What can you say about the RpnCalculator?
  • The x_register has a value of 99.
  • The x_register can still receive receive digits.
  • The first previous value is 14.
  • The second previous value is 7.

You already have validated the first two bullets with previous Examples, so there is no value in re-checking those features. You could add validation to existing examples to check for the second two values. I would advise against doing so because smaller tests tend to pinpoint problems better. Also, depending on the technology you are using, once a validation fails, subsequent validations are not checked in in RSpec, nor are the in most testing tools (FitNesse being one counter example).

Why is this last part important? When you've broken something, how big is the break? If your Examples fail, but each has one or a very small number of validations, you'll have a good idea of the span of the break. However, if you have Examples with dozens of validations (or many more), it is possible that an early validation will fail and you simply do not know if other validations would have failed. So while you think you only broke one thing, you broke 30 things. With many small tests, you'll have much higher fidelity information from which to move forward.

So with that in mind, you can continue with one a few more Examples working with <enter> to move towards validating the last two bullets.

Example
Here is an example that moves your production code just a little forward:
    it "should have two operands available after one <enter>" do
      @calculator.available_operands.should == 2
    end

This Example suggests that after the first <enter> you should have two operands available, the one on the stack and the x_register.

  • Create this example.

  • Add the missing method with an empty implementation.

  • Get the example to pass:
      def available_operands
        2
      end

  • Verify that all of your Examples now pass.

Check-In
  • Check in your work.

Refactor
There was little added to either of your two files, so there's not much to refactor right now. However, did you notice that you now have two hard-coded methods? This might suggest a direction in which you want to push your Examples.

Check-In
No need, no change.

Summary
This Example really just extended the API of the RpnCalcualtor. You might be feeling like it is time to get busy because of the two pending methods.

Example: Enter should increase the number of operands

The operator_count is hard-coded, this Example will make it harder to get away with that.

Example
    it "should increase the operand count after each <enter>" do
      enter_number 13
      @calculator.available_operands.should == 3
    end

  • Create this Example. Notice that since you added no new API calls to the RpnCalculator's interface, you can write the complete Example while following the second law of TDD/BDD.

  • Verify that your Example fails:
    - should increase the operand count after each <enter> (FAILED - 1)
     
    1)
    'RPN Calculator Handling the <enter> key should increase the operand count after each <enter>' FAILED
    expected: 3,
         got: 2 (using ==)
    ./rpn_calculator_spec.rb:62:
     
    Finished in 0.026028 seconds
     
    9 examples, 1 failure

There are two ways to make this work. Here's one way to get this to pass (don't do this):
Update initialize
  def initialize
    reset_input
    @op_count = 1
  end
Update execute_function
  def execute_function(function_symbol)
    @x_register_should_reset = true
    @op_count += 1
  end 
Update operand_count
  def available_operands
    @op_count
  end

Alternatively, you can use an array and actually store the values entered (notice, you change the same methods, add the same amount of code):
Update initialize
  def initialize
    reset_input
    @operands = []
  end
Update execute_function
  def execute_function(function_symbol)
    @x_register_should_reset = true
    @operands << x_register
  end
Update operand_count
  def available_operands
    @operands.length + 1
  end

Which of these is the least amount of code that will get the Example to pass? Both involve 3 lines of code added to 3 methods. One uses an integer, one uses an array, and an array is more complex than an integer, though in Ruby they are both objects.

Why do you try to do the least amount possible to get an Example to pass? That guideline is a "how" not a "what or a why". That is, it is a means to an end. What end? There are at least a few:
  • Writing only enough code means you are less likely to over-design or gold-plate your solution.
  • It also helps keep the production code actually covered by tests.

In this case, the Example will exercise all of the added code in both approaches. What about over-design? When are you over designing? Are you over designing when you are using a design pattern where just a simple direct relationship is adequate? Yes. Are you over-designing when you a complex object structure, when a small, flat structure will suffice? Yes. Is over designing always a problem? NO. The problem is not over design. The problem is that over design leads to more rework when you've guess wrong. And as a developer, I can say that I guess wrong often.

Why do developers guess wrong? Because we generally see problems differently that people who provide requirements. Here's an analogy. On the Nintendo WII, there's a feature called "Mii's". A Mii is an avatar. I only created one because I had to for some games. I created one and never change it. My wife and daughter both have crated more than one, they update their looks and I'm convinced if there was a game that simply let players updated their Avatars with clothes, and makeup, etc, they'd buy it. If you asked me, it's a waste of time. But that is one of the may appeals to the system.

Problems change in ways that we often cannot anticipate simply because they are coming from a very different place. So when you write more code than is necessary at the moment, you're risking having to change code, or maybe make something that is designed for unnecessary flexibility, which could make the code harder to adapt to actual changes on the ground.

In this example, the domain is somewhat technical. Also, you can read the manual for RPN calculators and they will talk about a stack. So using an array as this code does is not diverging from the problem domain.

The second solution is a good one and it is not over design given the domain context.

  • Implement the second solution.

  • Verify that all of your Examples pass.

Check-In
  • Check in your work.

Refactor
A quick review of the code shows a hard-coded top method. Now that you have a stack in place, can you simply return the "top" of the array?

  • Experiment, change the implementation of top:
      def top
        @operands.last
      end 

  • Run your Examples, everything still passes.

This works. You have changed the production code without breaking any tests, so this is a legitimate refactoring.

Check-In
  • Check in simple refactoring.

Summary
You have moved towards a more complete solution by introducing an array to hold operands. Was this over design? No. You certainly could have used the integer and then the stack, but that seems somewhat dogmatic.

You also managed to get rid of the last hard-coded method, top. This one Example managed to move the production code forward quite a bit.

Example: The right thing is on the stack

Right now, there's no way to know if what is on the stack is the right thing on the stack. Most of the pieces are in place, in fact even though the code is fully covered by the Examples, the Examples do not reflect fully what your production code can do. It is time to exploit that.

Example
Here is an example that verifies the stack behavior of the RpnCalculator:
  describe "Stack" do
    it "should contain the correct items in the correct order" do
      enter_number 4
      enter_number 19
      enter_number -1
      @calculator.pop!.should == -1
      @calculator.pop!.should == 19
      @calculator.pop!.should == 4
    end
  end 

First observation: This code will not run because there is no pop! method on the RpnCalcualtor.

  • Create the Example, verify that the test fails as expected.

  • Add an empty pop! method to verify that the Example fails for the right reason:
      def pop!
      end

  • Run your Examples.

  • Implement pop!:
      def pop!
        @operands.pop
      end

  • Verify that your Examples pass.

Check-In
  • Check in your work.

Refactor
There has been a slowly-growing code small, Feature Envy, but hold off until just a bit longer. The "describe" part of the last Example is a huge clue.

Check-In
No need, you did not do any refactoring.

Summary
You have added an example to verify that in fact the right values are getting put onto the stack. Your RpnCalculator is growing, but at what point is the RpnCalcualtor suffering from too much design debt? It is right now, but you'll see that after the next Example.

Example: What happens with an 'Empty' stack?

The stack on an RpnCalculator is conceptually filled with 0's when it has no values. In fact, it is literally filed with 0's. So if you check the top of your RpnCalculator, calculator, it should return 0 when it is empty. The same is true for pop!. It should return 0 if the stack is empty.

Example
These two are closely related, so here, for the first time, are two complete examples that you will fix at the same time.

    it "should return 0 when pop!'ing from an empty stack" do
      @calculator.pop!.should == 0
    end
 
    it "should return 0 for top when stack empty" do
      @calculator.top.should == 0
    end

  • Create these Examples, they will both fail the same way:
    - should return 0 when pop!'ing from an empty stack (FAILED - 1)
    - should return 0 for top when stack empty (FAILED - 2)
     
    1)
    'RPN Calculator Stack should return 0 when pop!'ing from an empty stack' FAILED
    expected: 0,
         got: nil (using ==)
    ./rpn_calculator_spec.rb:77:
     
    2)
    'RPN Calculator Stack should return 0 for top when stack empty' FAILED
    expected: 0,
         got: nil (using ==)
    ./rpn_calculator_spec.rb:81:
     
    Finished in 0.031744 seconds
     
    12 examples, 2 failures

  • When code pop's values off of an empty array, the array returns nil. The same thing happens when code calls top. This will fix both of your failing Examples:
      def top
        @operands.length > 0 ? @operands.last : 0
      end
     
      def pop!
        @operands.length > 0 ? @operands.pop : 0
      end

  • Make these changes and verify all of your Examples are passing.

Check-In
Check in, it is finally time to get busy removing Feature Envy and get closer to conforming to the Single Responsibility Principle.

Refactor
Review the following methods: pop!, top, available_operators. What do they all have in common? All three of these methods deal exclusively with the @operands instance variable.

Is this bad? Yes, it suggests that the RpnCalculator is doing work that should be pushed into the array. What? That's right, the array is not pulling its weight. This is a legitimate case for wrapping a collection class and giving it domain-specific behavior (semantics).

There is a name for methods in classes working exclusively with data in other objects. It is a code smell called Feature Envy.

Normally, at this point I'd say "you cannot change a system-defined class, so you either need to subclass or wrap and adapt". That is, create a subclass of Array, change the top method and add the pop method. Or, create a new class called something like OperandStack that holds a single instance of an array.

However, in Ruby, classes are never closed. You can add methods to a class anytime. Adding methods to Array is a bad idea since the Array class is used all over the place. However, Ruby also allows you to add methods to an instance.

This is crazy. Here is an example of how you could do that (don't do this):
  def create_operand_stack
    stack = []
    def stack.pop!
      return length > 0 ? super : 0
    end
 
    def stack.top
      return length > 0 ? last : 0
    end
    stack
  end
 
  custom_stack = create_operand_stack
 
  puts custom_stack.top == 0
  puts custom_stack.pop! == 0

The result from running this will be "true" printed twice. But if you try to simply create an array, you will not see the same results. Neither method is defined on the Array class.

Barring extending a single instance, it is certainly reasonable to create an OperandStack class, move these methods into it and then change the implementation of the RpnCalcualtor to use the OperandStack.

Before you start, did you notice that the Context for the last three Examples was "Stack"? That's a big clue that there's a missing class.

  • There are 3 Examples in the "Stack" context. Move the first one out into "Handling the <enter> key" context.

  • Verify that all of your Examples are still passing.

Next, you are going to do some "big" changes. It won't take long, but this will be the longest amount of time the tutorial will have you work before you can see your Examples running. Note your reaction to the change.

  • Change the structure of rpn_calculator_spec.rb:
    describe "RPN Calculator" do
      ...
    end
     
    describe "Stack" do
      ...
    end

If you ran your examples now, the ones in the Stack context will fail. Why? Because they no longer pick up the automatically-created calculator object form the before in the "RPN Calculator" context.

  • Update the "Stack" context:
    describe "OperandStack" do
      before (:each) do
        @stack = OperandStack.new
      end
     
      it "should return 0 when pop!'ing from an empty stack" do
        @stack.pop!.should == 0
      end
     
      it "should return 0 for top when stack empty" do
        @stack.top.should == 0
      end
    end

This still won't pass. There's no OperandStack class.

  • Move the entire OperandStack context into a new file called "operand_stack_spec.rb".

  • Now your rpn_calculator_spec will pass:
    Macintosh-7% spec -f s -c rpn_calculator_spec.rb     
     
    RPN Calculator
     
    RPN Calculator Basic Creation
    - should be non-null after creation
     
    RPN Calculator Handling integers
    - should store a series of digits in the x register
    - should handle a string of digits with an embedded .
     
    RPN Calculator Handling the <enter> key
    - should copy x_register to top of stack
    - should cause next digit to reset the x_register
    - should allow the x_register to be <entered> again
    - should only reset after the first digit and not subsequent digits
    - should have two operands available after one <enter>
    - should increase the operand count after each <enter>
    - should contain the correct items in the correct order
     
    Finished in 0.027715 seconds
     
    10 examples, 0 failures

  • Next, create a new class called OperandStack in a file called opeand_stack.rb:
    class OperandStack
      def initialize
        @operands = []
      end
     
      def top
        @operands.length > 0 ? @operands.last : 0
      end
     
      def available_operands
        @operands.length + 1
      end
     
      def pop!
        @operands.length > 0 ? @operands.pop : 0
      end
     
      def <<(value)
        @operands << value
      end
    end

Note that these are mostly just copies of methods from RpnCalculator. There is one new method, <<, which just delegates to a method of the same name.

  • Verify that your operand_stack_spec.rb now passes:
    Macintosh-7% spec -f s -c operand_stack_spec.rb
     
    OperandStack
    - should return 0 when pop!'ing from an empty stack
    - should return 0 for top when stack empty
     
    Finished in 0.013598 seconds
     
    2 examples, 0 failures

Did you fully test this class? No. What you instead did was extract a class and then copy the tests that were directly testing it. You are not writing new code, you are refactoring the solution.

  • Run all of your Examples, everything should be passing:
    Macintosh-7% spec -f s -c *_spec.rb
     
    OperandStack
    - should return 0 when pop!'ing from an empty stack
    - should return 0 for top when stack empty
     
    RPN Calculator
     
    RPN Calculator Basic Creation
    - should be non-null after creation
     
    RPN Calculator Handling integers
    - should store a series of digits in the x register
    - should handle a string of digits with an embedded .
     
    RPN Calculator Handling the <enter> key
    - should copy x_register to top of stack
    - should cause next digit to reset the x_register
    - should allow the x_register to be <entered> again
    - should only reset after the first digit and not subsequent digits
    - should have two operands available after one <enter>
    - should increase the operand count after each <enter>
    - should contain the correct items in the correct order
     
    Finished in 0.029495 seconds
     
    12 examples, 0 failures

Intra-refactoring Check-In
  • All of your Examples are passing, right? Now is a great time to check in before moving to the next step.

Back to refactoring
Now you should refactor the RpnCalcualtor to use your newly-created class. As with other refactorings, you'll add, duplicate, validate and then remove code.

  • Add "require 'operand_stack' to the beginning of your rpn_calculator.rb file.

  • Update the initialize method:
      def initialize
        reset_input 
        @operands = []
        @operand_stack = OperandStack.new
      end

  • Run your Examples, everything should still pass.

  • Update execute_function to duplicate the work of storing the x_register:
      def execute_function(function_symbol)
        @x_register_should_reset = true
        @operands << x_register
        @operand_stack << x_register
      end

  • Run your Examples, everything should still pass.

  • Update top:
      def top
        @operand_stack.top
      end

  • Run your Examples, everything should still pass.

  • Update pop!:
      def pop!
        @operand_stack.pop!
      end

  • Reviewing available_operands, you notice that there's no length method on Operand stack. So add it:
      def length
        @operands.length
      end

  • Run your Examples, everything should still pass.

  • Update available_operands:
      def available_operands
        @operand_stack.length + 1
      end

  • At this point, you can remove all code that refers to @operands (you'll change 2 places).

  • Run your Examples, everything should still pass.

Check-In
Whew! That was a big refactoring. Check your code in!

Summary
This was a big change. You had a violation of feature envy. The RpnCalculator was doing a lot of work with information stored in an Array. So you created a class to represent that work, OperandStack, and moved Examples around to get everything situated.

If you have a decent IDE, these kinds of refactorings are quick, easy and fairly reliable. If not, then like Robert Martin tweeted:
  • It's like riding a horse on the interstate.

This cleaned up your RpnCalculator class but there's still more to be done. The implementation of execute_function is wanting to do more (or less).

Is there a problem with how you are testing OpernadStack? There are only 2 Examples but quite a few methods for which you do not have unit tests. Those methods are in fact used from the RpnCalculator so you have coverage. However, if the OperandStack is used by other classes, then the testing is not adequate. Maybe you should consider a nested class. Maybe you should write more Examples to get its "contract" fully specified.

When you are just learning TDD or BDD, this kind of stuff is going to happen. Let's leave it alone for now and see what happens.

Sidebar.jpgSidebar: Wrapping Collections
Do it!

I was tempted to just leave it at that, but I'll say a bit more. In my experience, most (anything beyond a trivial problem and most trivial problems) of the time I use a collection, at some point I'll want more behavior that the collection offers. The collection implements a "raw" zero-to-many relationship. Most of the time, my requirements want more than just a "raw" relationship. In the case of the RpnCalculator, it's stack is of infinite size and it always has zero after anything else that is pushed onto it. We simulated this by returning 0 if the stack is in fact empty.

It is nearly always a good idea to augment a system-defined collection with your own flair.

When you do, you have several options:
  • Subclass (in .Net you don't have this option because most methods are sealed and non-virtual)
  • Wrap and delegate (this is what we did). You always have this option
  • Open up the class (Ruby and other dynamic languages) - this is dangerous. Opening up a heavily used class like this is "too cool."
  • Open up an instance (shown above) - this is probably too cool as well.

The option that always works, though it might require a touch more work, is the second option and what you did in this tutorial.

If you every have a collection within another collection, it's immediately time to follow this recommendation.
Sidebar.jpg

The Classes So Far
Here's one example of what all of your code should look like about now.
rpn_calculator_spec.rb
require 'rpn_calculator'
 
describe "RPN Calculator" do
  before(:each) do
     @calculator = RpnCalculator.new
  end 
 
  def enter_number(number) 
    number.to_s.each_char{ |d| @calculator.digit_pressed d }
    @calculator.execute_function(:enter)
  end
 
  describe "Basic Creation" do
    it "should be non-null after creation" do
      @calculator.should_not be nil
    end
  end
 
  describe "Handling integers" do
    it "should store a series of digits in the x register" do
      enter_number 42
      @calculator.x_register.should == 42
    end
 
    it "should handle a string of digits with an embedded ." do
      enter_number 13.31
      @calculator.x_register.should == 13.31
    end
  end
 
  describe "Handling the <enter> key" do
    before(:each) do
       enter_number 654
    end
 
    it "should copy x_register to top of stack" do
      @calculator.top.should == 654
    end
 
    it "should cause next digit to reset the x_register" do
      @calculator.digit_pressed 1
      @calculator.x_register.should == 1
    end
 
    it "should allow the x_register to be <entered> again" do
      @calculator.execute_function(:enter)
      @calculator.top.should == 654
      @calculator.x_register.should == 654
    end
 
    it "should only reset after the first digit and not subsequent digits" do
      enter_number 27
      @calculator.x_register.should == 27
    end
 
    it "should have two operands available after one <enter>" do
      @calculator.available_operands.should == 2
    end
 
    it "should increase the operand count after each <enter>" do
      enter_number 13
      @calculator.available_operands.should == 3
    end
 
    it "should contain the correct items in the correct order" do
      enter_number 4
      enter_number 19
      enter_number -1
      @calculator.pop!.should == -1
      @calculator.pop!.should == 19
      @calculator.pop!.should == 4
    end
  end
end

rpn_calculator.rb
require 'operand_stack'
 
class RpnCalculator
  def initialize
    reset_input
    @operand_stack = OperandStack.new
  end
 
  def digit_pressed(digit)
    reset_input if @x_register_should_reset
    @input << digit.to_s
  end
 
  def reset_input
    @input = ''
    @x_register_should_reset = false
  end
 
  def x_register
    @input.to_f
  end
 
  def execute_function(function_symbol)
    @x_register_should_reset = true
    @operand_stack << x_register
  end
 
  def top
    @operand_stack.top
  end
 
  def available_operands
    @operand_stack.length + 1
  end
 
  def pop!
    @operand_stack.pop!
  end
end

operand_stack_spec.rb
require "operand_stack"
 
describe "OperandStack" do
  before (:each) do
    @stack = OperandStack.new
  end 
 
  it "should return 0 when pop!'ing from an empty stack" do
    @stack.pop!.should == 0
  end 
 
  it "should return 0 for top when stack empty" do
    @stack.top.should == 0
  end 
end 

operand_stack.rb
class OperandStack
  def initialize
    @operands = []
  end 
 
  def top
    @operands.length > 0 ? @operands.last : 0
  end
 
  def available_operands
    @operands.length + 1
  end
 
  def pop!
    @operands.length > 0 ? @operands.pop : 0
  end
 
  def <<(value)
    @operands << value
  end
 
  def length
    @operands.length
  end
end 

Example: A Binary Operator

Some time ago you created your first Example to exercise the <enter> key. That resulted in the following method:
  def execute_function(function_symbol)
    @x_register_should_reset = true
    @operand_stack << x_register
  end

Now it is time start executing more functions. The first function you'll support is +.

Example
Here is a basic example to get started:
  describe "Basic Math Operators" do
    it "should add the x_regiser and the top of the stack" do
      enter_number 46
      @calculator.execute_function(:+)
      @calculator.x_register.should == 92
    end
  end

Why is the result 92? The support method enter_number
  • Create this Example and run it. The result is not quite right:
    1)
    'RPN Calculator Basic Math Operators should add the x_regiser and the top of the stack' FAILED
    expected: 700,
         got: 46.0 (using ==)
    ./rpn_calculator_spec.rb:79:
     
    Finished in 0.03118 seconds
     
    13 examples, 1 failure

  • Getting this to pass is a "snap":
      def execute_function(function_symbol)
        if function_symbol == :enter
          @x_register_should_reset = true
          @operand_stack << x_register
        elsif
          @input = 92.to_s
        end
      end

  • Get your Examples passing.

Check-In
Check in your code. You are about to refactor.

Refactor
OK, the method you just updated, execute_function, is a bit of a mess. It does two thigns:
  • Determine which function to perform
  • Actually perform the work.

So you'll separate this for now.

  • Extract two methods, enter, +:
      def enter
        @x_register_should_reset = true
        @operand_stack << x_register
      end 
     
      def +
        @input = 92.to_s
      end

  • Make sure your Examples still pass.

  • Update the execute_function to use those two methods:
      def execute_function(function_symbol)
        if function_symbol == :enter
          enter
        else
          self.+
        end
      end

  • Update your RpnCalculator, verify that your Examples are all passing.

Check-In
This is all the refactoring you'll do right now. More is on the horizaon.

Summary
You've improved the execute_function but you have a stubbed-out + method. If you know a bit of Ruby, you might be chomping at the bits to evaluate a symbol, or use lambda and a hash. The code will get where it will, however, based on the Examples, rather than speculation.

Example: Work on +

What should happen with + when there are no operands? Should it fail? Should it result in 0? If you're simulating an HP calculator, the result is 0. However, what it should do will be defined by the Examples you write.

Example
    it "should result in 0 when the calculator is empty" do
      @calculator.execute_function(:+)
      @calculator.x_register.should == 0
    end

  • Create this Example. Verify that it fails.

  • Update the + method to actually do some work.
      def enter
        @x_register_should_reset = true
        @operand_stack << x_register
      end

  • Run your Example, make sure it works.

Check-In
Check in your work.

Refactor
Something is starting to look strange. You may have noticed it the first time your code "supported" +. The following line:
    @input = result.to_s

I'm not sure what to do with that yet because there's not enough code to make many decisions. It might be that the code will stay put. It just seems a bit strange.

Check-In
No refactoring done, no need to check in again.

Summary
The + method no does work. It has one strange line, and there's still the issue of whether it modifies the stack correctly or not. Another thing occurred to me as well. After any operator, the x_register should be in "reset" mode.

Example: x_register in reset mode after +

Like <enter>, after perform +, the next character typed should clear the x_register and write into it. That seems like a good thing to check next.

Example
    it "should reset the x_register after +" do
      enter_number 99
      @calculator.execute_function(:+)
      @calculator.digit_pressed 8
      @calculator.x_register.should == 8
    end

  • Create this Example. Run it and you'll notice that it fails:
    1)
    'RPN Calculator Basic Math Operators should reset the x_register after +' FAILED
    expected: 8,
         got: 9.08 (using ==)
    ./rpn_calculator_spec.rb:91:
     
    Finished in 0.032479 seconds
     
    16 examples, 1 failure

  • Make a quick change to the + method to fix this:
      def +
        lhs = @operand_stack.pop!
        rhs = x_register
        result = lhs + rhs
        @input = result.to_s
        @x_register_should_reset = true
      end

  • Run your Examples, they should all pass.

Check-In
Check in your code. This time you will do a little bit of refactorig.

Refactor
There is a duplicated line in the enter and + methods:
    @x_register_should_reset = true

At the very least, this line of code could be put into a well-named method.

  • Create a new method (if your IDE supports Extract method, use it):
      def set_override_mode
        @x_register_should_reset = true
      end

  • Make sure your Examples still pass.

  • Update the enter and + methods to call this new method.

  • Make sure your Examples still pass.

Check-In
Even though small, you did make a change to your solution while keeping your Examples passing, so check in your work.

Summary
There appears to be a pattern emerging. Two functions, enter and +, both of which change the calculator into "override_mode". Will other functions look like this? Yes. Will you be able to remove this duplication as well? Yes.

Example: What about the stack?

Some time back the tutorial asked about whether the stack was treated properly. You could have put those checks into another Example, but the rationale for not doing that is to keep your tests small and focused for better pinpointing of problems and better assessment of the size of a break.

It's time to come back to that, if for no other reason than to expression a "contract" that the + method (and all binary operators) will need to follow.

Example
Here's an example:
    it "should should reduce the stack by one" do
      enter_number 9
      enter_number 8
      @calculator.execute_function :+
      @calculator.available_operands.should == 2
    end

  • Create this example.

  • Run your Examples. Notice it passes.

Did we code too much or are we validating a behavior? Would it have been possible to write less code and still keep other tests passing? Maybe, the design is such that it's getting harder to do the wrong thing. In any case, this seems like an OK Example - if maybe a bit off target.

Check-In
Check in your work.

Refactor
There's starting to be a bit of cruft in the Examples and maybe something in the RpnCalcualtor. You might already be noticing yourself. For example, in RpnCalculator, to set the "x_register" the code does the following:
    @input = result.to_s
    set_override_mode
That is not self evident. In addition, getting two operands for a binary operator involves two very different expressions:
    lhs = @operand_stack.pop!
    rhs = x_register

The last two Examples are OK, though do you believe that the differences between digit_pressed and enter_number is obvious?

Start by making an improvement to the bottom part of the + method:
  • Add a new method that is private (since private sets subsequent methods, make this the last method in your RpnCalculatorClass):
      private
      def reset_x_register(value)
        @input = value.to_s
        set_override_mode
      end

  • Verify your Examples still pass.

  • Update +:
      def +
        lhs = @operand_stack.pop!
        rhs = x_register
        result = lhs + rhs
        reset_x_register result
      end

  • Verify your Examples still pass.

  • Check in this refactoring.

The next thing you might consider is how to better document getting parameters for +. I tried a few ways but none seemed to really make the code better. Just differently convoluted. You might try yourself (and even succeed). However, I already have an idea of where I want this code to go, so rather than any more refactoring, I want to move into other operators.

Check-In
  • Check in your refactoring.

Summary
You now have the basics of the + operator worked out. You make resetting the x_register at least a little better. Now it is time to add some more operators to see how this current solution grows.

Example: Support for a new binary operator, -

Adding support for - will allow you to see how your solution grows as new math functions are supported. You have already vetted + fairly well, so adding - should be a snap.

Example
    it "should subtract the first number entered from the second" do
      enter_number 4
      @calculator.digit_pressed 9
      @calculator.execute_function :-
      @calculator.x_register.should == -5
    end

At this point, hopefully you are getting bothered by the difference between lines that start with an @ and ones that do not. Add that to your punch-list.

  • Create this example. Run your examples and verify that they do not pass.

Were you surprised by the result? The results suggest that instead of -, your calculator performed +. Why is that?

A quick review of execute_function should give you a clue:
  def execute_function(function_symbol)
    if function_symbol == :enter
      enter
    else
      self.+
    end
  end

So if something is not :enter, then it is :+. Add that to your punch-list:
  • Your execute_function should fail if the operator is not found.

  • However, you are not working on that Example. So update this method to call -:
      def execute_function(function_symbol)
        if function_symbol == :enter
          enter
        elsif function_symbol == :+
          self.+
        elsif function_symbol == :-
          self.-
        end
      end

  • Now your code is missing the function - (which you discovered by running your Examples, right?), so add it:
      def -
      end

  • Run your Example, now it should be failing for the right reason (value did not match).

You have two obvious choices here:
  • Create a method that is hard-coded to return the correct value.
  • Create a duplicate of the + method and change it to be -

  • Rather than following the third rule, you can safely duplicate the + method and update it (you'll be refactoring this soon anyway):
      def -
        lhs = @operand_stack.pop!
        rhs = x_register
        result = lhs - rhs
        reset_x_register result
      end

  • Verify that all of your Examples are passing.

Check-In
Check in, you have a lot on your punch-list.

Refactor
There are several things on your punch-list:
  1. Adding a new operator requires too much work: update a method, add a new method, and write that method. More importantly, it requires changing an existing method which is in direct violation of the open/close principle.
  2. The execute_function does not indicate if the function does not exist.
  3. There is duplicated code between the + and - methods. (This was on your list, wasn't it?)
  4. The difference between enter_number and digit_pressed seems isn't obvious.
  5. The Examples should not have such a mix of lines that use fields and support methods.

Looks like you have a lot of work.

First, if you have been chomping at the bits to fix execute_function, now is the time to do so:

  • Update execute_function:
      def execute_function(function_symbol)
        self.send(function_symbol)
      end

  • Run your Examples to verify that they all pass.

This is a Ruby idiom. The original approach of passing in a symbol screamed for this solution. This has several effects
  • Removes the need to update this method ever again
  • Requires, however, that the functions of the calculator are actually methods on the class.
  • Your code now will fail if you attempt to send a method that is unknown
  • Allows invocation of (0 argument) private methods

Even so, this is an improvement and it follows a Ruby idiom.

  • Check in your change.

Next, you'll remove the violation of the DRY principle:

  • Add a new private method to your RpnCalculator method:
      def execute_binary_operator
        lhs = @operand_stack.pop!
        rhs = x_register
        result = yield lhs, rhs
        reset_x_register result
      end

  • Make sure your Examples still pass.

  • Update the + method:
      def +
        execute_binary_operator { |lhs, rhs| lhs + rhs }
      end

  • Run your Examples, make sure they still pass.

  • Update the - method:
      def +
        execute_binary_operator { |lhs, rhs| lhs + rhs }
      end

  • Run your Examples, make sure they still pass.

Looks like you've managed to fix the first three things on your punch-list with a few quick changes in your code. Of course, you still do not have an Example that captures what happens when you execute a bogus method, the code should handle that now.

Before calling this refactoring session done, clean up your Examples to make this a bit more self-explanatory.

  • Add some new support methods (put these at the same place as enter_number):
      def type(number)
        number.to_s.each_char{ |d| @calculator.digit_pressed d }
      end
     
      def press(a_symbol)
        @calculator.send(a_symbol)
      end
     
      def validate_x_register(expected)
        @calculator.x_register.should == expected
      end

  • Make sure your examples still pass.

  • Update "should subtract the first ...":
        it "should subtract the first number entered from the second" do
          type 4
          press :enter
          type 9
          press :-
          validate_x_register -5
        end 

  • Run your Examples, they should all pass.

Sidebar.jpgSidebar: Example styles and Your audience
Which do you prefer? You have seen three different forms:
  1. First, you saw Examples simply using the calculator.
  2. Second, you saw Examples using a combination of support methods and using the calculator.
  3. Now, you see an Example only using support methods.

The first form gives a concrete example of how the RpnCalculator should be used. The last form makes the Example easy to read and moves the writing of the examples into the way a user might speak about using the calculator.

Here is just he Basic Math Operators context updated:
  describe "Basic Math Operators" do
    it "should add the x_regiser and the top of the stack" do
      type 46
      press :enter
      press :+
      validate_x_register 92
    end
 
    it "should result in 0 when the calculator is empty" do
      press :+
      validate_x_register 0
    end
 
    it "should reset the x_register after +" do
      type 9
      press :+
      type 8
      validate_x_register 8
    end
 
    it "should should reduce the stack by one" do
      type 9
      press :enter
      type 8
      press :enter
      press :+
      @calculator.available_operands.should == 2
    end
 
    it "should subtract the first number entered from the second" do
      type 4
      press :enter
      type 9
      press :-
      validate_x_register -5
    end
  end
Here's a side-by-side comparison between the two styles:

it "programmer" do
  • @calculator.digit_pressed 4
  • @calculator.execute_function :enter
  • @calculator.digit_pressed 5
  • @calculator.digit_pressed 2
  • @calculator.execute_function :+
  • @calculator.x_register.should == 56
end
it "user" do
  • type 4
  • press :enter
  • type 52

  • press :+
  • validate_x_register 56
end


Before thinking that one style is the "right" style, consider the audience and even the author. If the audience is the user, then the style on the right is more appropriate. If the audience is another developer, then the left side might be more appropriate.


You might think that using the methods names on the right for your calculator is an option. However, that won't look correct:
  @calculator.type 4
  @calculator.press :enter
  @calculator.type 52
  @calculator.press :+
  @calculator.x_register.should == 56
The method names do not make sense being sent to a calculator. So the method names make sense on the left when sent to a calculator. The method names on the right make sense when reading an example.

While this tutorial will not cover it, RSpec has the notion of "story tests", or tests that are meant to be written by the same people who write user stories, your customer, product owner, QA person or even developers. That's coming up in a later tutorial, so this subject will come up again.
Sidebar.jpg

However, just to place a (somewhat arbitrary) stake in the ground, here's an updated version of rpn_calculator_spec.rb:
require 'rpn_calculator'
 
describe "RPN Calculator" do
  before(:each) do
     @calculator = RpnCalculator.new
  end
 
  def type(number)
    number.to_s.each_char{ |d| @calculator.digit_pressed d }
  end
 
  def press(a_symbol)
    @calculator.execute_function a_symbol
  end
 
  def validate_x_register(expected)
    @calculator.x_register.should == expected
  end
 
  def validate_top(expected)
    @calculator.top.should == expected
  end
 
  def validate_operands(expected)
    @calculator.available_operands.should == expected
  end
 
  def validate_pop!(expected)
    @calculator.pop!.should == expected
  end
 
  describe "Basic Creation" do
    it "should be non-null after creation" do
      @calculator.should_not be nil
    end
  end
 
  describe "Handling integers" do
    it "should store a series of digits in the x register" do
      type 42
      press :enter
      validate_x_register 42
    end
 
    it "should handle a string of digits with an embedded ." do
      type 13.31
      validate_x_register 13.31
    end
  end
 
  describe "Handling the <enter> key" do
    before(:each) do
      type 654
      press :enter
    end
 
    it "should copy x_register to top of stack" do
      validate_top 654
    end
 
    it "should cause next digit to reset the x_register" do
      @calculator.digit_pressed 1
      @calculator.x_register.should == 1
    end
 
    it "should allow the x_register to be <entered> again" do
      press :enter
      validate_top 654  
      validate_x_register 654
    end
 
    it "should only reset after the first digit and not subsequent digits" do
      type 27
      validate_x_register 27
    end
 
    it "should have two operands available after one <enter>" do
      validate_operands 2
    end
 
    it "should increase the operand count after each <enter>" do
      type 13
      press :enter
      validate_operands 3
    end
 
    it "should contain the correct items in the correct order" do
      type 4
      press :enter
      type 19
      press :enter
      type -1
      press :enter
      validate_pop! -1
      validate_pop! 19
      validate_pop! 4
    end
  end
 
  describe "Basic Math Operators" do
    it "should add the x_regiser and the top of the stack" do
      type 46
      press :enter
      press :+
      validate_x_register 92
    end
 
    it "should result in 0 when the calculator is empty" do
      press :+
      validate_x_register 0
    end
 
    it "should reset the x_register after +" do
      type 9
      press :+
      type 8
      validate_x_register 8
    end
 
    it "should should reduce the stack by one" do
      type 9
      press :enter
      type 8
      press :enter
      press :+
      @calculator.available_operands.should == 2
    end
 
    it "should subtract the first number entered from the second" do
      type 4
      press :enter  
      type 9
      press :-
      validate_x_register -5
    end
  end
end

  • Get to where all of your Examples are passing.

Check-In
  • Check in your work.

Summary
That was a lot of refactoring. Maybe the tutorial let you collect too much design debt. However, this will happen and it is up to you to pick up the slack.

You made several important changes:
  • The execute_function method now uses the send method to execute a symbol, so you will not have to update it so long as new functions are added to the RpnCalculator clss.
    As a result of this change, your execute_function will indicate errors better than when you added an Example for - (which caused + to get called)
  • You created an execute_binary_operator method that makes adding binary operators a one-line lambda expression.
    As a result, you removed some DRY violations.
  • You made a significant change to how you write your examples.

What's left? There's still the issue of documenting what happens when you attempt to execute a missing function and the rpn_calculator_spec.rb file is getting large.

This second issue is not addressed in this tutorial.

Example: Handling missing functions

So what happens when your calculator is asked to execute an unknown function?

Example
  describe "Handling Functions" do
    it "should raise an exception if attempting to run missing function" do
      lambda { press :bogus_function_name }.should raise_error NoMethodError
    end
  end

  • Create this new Context and verify that it passes.

This Example documents that you expect the behavior of throwing a NoMethodError when sending a bogus method name to your calculator.

Check-In
  • Check in your code.

Refactor
You just did a bunch of refactoring. You only added an Example to capture something that was added during refactoring but not explicitly spelled out in any Example. So nothing new to refactor.

Check-In
You have no changes to check in.

Summary
This example really just captured something about your system that was not explicitly spelled out anywhere. This probably indicates too much change done during refactoring. It would have been possible for you to wait to change the implementation of execute_function and first write this Example.

You won't always figure out to do that, so you've managed to recover gracefully.

Example: Add support for * and /

You have a working design for + and -, adding * and / should be a snap. First your calculator will support multiplication.

Example
    it "should multiply the first number entered by the second" do
      type 3
      press :enter 
      type 9
      press :*
      validate_x_register 27
    end

  • Create this example. Run it and verify that your test fails with a missing method.

  • Create the method:
      def *
        execute_binary_operator { |lhs, rhs| lhs * rhs }
      end 

  • Verify that your Examples pass.

That was so quick, before checking in add one more example:
    it "should divide the first number entered by the second" do
      type 9
      press :enter
      type 3
      press :/
      validate_x_register 3
    end

  • Add the missing method:
      def /
        execute_binary_operator { |lhs, rhs| lhs / rhs }
      end

  • Verify that your Examples pass.

Check-In
Check in your work.

Refactor
You might review your code and find some things to refactor. One issue is the size of the calculator. Right now it is not too bad, but it's going to get bigger. Each time you add a function, it grows.

Check-In
Nothing new to check in.

Summary
You've just finished out the basics for your calculator.



RUNNING OUT, COPY AGAIN


Example x

Overview
Example
Check-In
Refactor
Check-In
Summary

Example x

Overview
Example
Check-In
Refactor
Check-In
Summary

Back